package com.zjmzxfzhl.integrityconstraint.annotation;

import cn.hutool.core.util.StrUtil;
import com.zjmzxfzhl.integrityconstraint.config.IntegrityConstraintProperties;
import com.zjmzxfzhl.integrityconstraint.entity.IntegrityConstraint;
import com.zjmzxfzhl.integrityconstraint.exception.IntegrityConstraintException;
import com.zjmzxfzhl.integrityconstraint.pojo.ToCheck;
import com.zjmzxfzhl.integrityconstraint.service.IntegrityConstraintService;
import com.zjmzxfzhl.integrityconstraint.utils.JacksonUtil;
import io.github.resilience4j.spelresolver.SpelRootObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MapAccessor;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 完整性验证切面处理
 *
 * @author 庄金明
 */
@Aspect
@Slf4j
public class IntegrityConstraintAnnotationAspect implements Ordered {

    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    private final IntegrityConstraintService integrityConstraintService;
    private final IntegrityConstraintProperties integrityConstraintProperties;
    private final BeanFactoryResolver beanFactoryResolver;
    private final SpelExpressionParser spelExpressionParser;

    public IntegrityConstraintAnnotationAspect(IntegrityConstraintService integrityConstraintService, IntegrityConstraintProperties integrityConstraintProperties, BeanFactory beanFactory, SpelExpressionParser spelExpressionParser) {
        this.integrityConstraintService = integrityConstraintService;
        this.integrityConstraintProperties = integrityConstraintProperties;
        this.beanFactoryResolver = new BeanFactoryResolver(beanFactory);
        this.spelExpressionParser = spelExpressionParser;
    }

    @Pointcut(value = "@within(integrityConstraintAnnotation) || @annotation(integrityConstraintAnnotation)", argNames = "integrityConstraintAnnotation")
    public void matchAnnotatedMethod(IntegrityConstraintAnnotation integrityConstraintAnnotation) {
    }

    @Around(value = "matchAnnotatedMethod(integrityConstraintAnnotation)", argNames = "proceedingJoinPoint, integrityConstraintAnnotation")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint,
                               IntegrityConstraintAnnotation integrityConstraintAnnotation) throws Throwable {
        if (integrityConstraintAnnotation.operationType().equals(OperationTypeEnum.SAVE)) {
            return save(proceedingJoinPoint, integrityConstraintAnnotation);
        } else if (integrityConstraintAnnotation.operationType().equals(OperationTypeEnum.CHECK)) {
            return check(proceedingJoinPoint, integrityConstraintAnnotation);
        } else {
            throw new IntegrityConstraintException("invalid IntegrityConstraintAnnotation.operationType[" + integrityConstraintAnnotation.operationType() + "]");
        }
    }

    private Object check(ProceedingJoinPoint proceedingJoinPoint, IntegrityConstraintAnnotation integrityConstraintAnnotation) throws Throwable {
        MethodBasedEvaluationContext evaluationContext = getEvaluationContext(proceedingJoinPoint);
        String businessKey = (String) parseExpression(integrityConstraintAnnotation.businessKey(), evaluationContext);
        List<ToCheck> toChecks = new ArrayList<>();
        VerifyAnnotation[] verifyAnnotations = integrityConstraintAnnotation.checkVerifys();
        for (VerifyAnnotation verifyAnnotation : verifyAnnotations) {
            Boolean verifiable = true;
            if (StrUtil.isNotBlank(verifyAnnotation.conditionExpression())) {
                verifiable = parseExpressionBoolean(verifyAnnotation.conditionExpression(), evaluationContext);
            }
            if (verifiable) {
                Map<String, Object> checkData;
                if (verifyAnnotation.values().length > 0) {
                    checkData = resolve(verifyAnnotation.values(), evaluationContext);
                } else {
                    checkData = resolve(verifyAnnotation.valuesExpression(), evaluationContext);
                }
                ToCheck toCheck = ToCheck.builder().type(verifyAnnotation.type()).data(checkData).build();
                toChecks.add(toCheck);
            }
        }

        boolean success = false;
        try {
            log.info("businessKey={}, check begin", businessKey);
            success = this.integrityConstraintService.check(businessKey, toChecks);
            if (!success) {
                log.info("businessKey={}, check fail", businessKey);
                throw new IntegrityConstraintException(integrityConstraintProperties.getErrorMessage());
            }
            return proceedingJoinPoint.proceed();
        } finally {
            if (success) {
                try {
                    this.integrityConstraintService.delete(businessKey);
                    log.info("businessKey={}, check end", businessKey);
                } catch (Exception e) {
                    log.error("删除缓存失败，但忽略该异常：", e);
                }
            }
        }
    }

    private Object save(ProceedingJoinPoint proceedingJoinPoint, IntegrityConstraintAnnotation integrityConstraintAnnotation) throws Throwable {
        MethodBasedEvaluationContext evaluationContext = getEvaluationContext(proceedingJoinPoint);
        String businessKey = (String) parseExpression(integrityConstraintAnnotation.businessKey(), evaluationContext);
        VerifyAnnotation verifyAnnotation = integrityConstraintAnnotation.saveVerify();
        String verifyConditionExpression = verifyAnnotation.conditionExpression();
        boolean saveable = true;
        if (StrUtil.isNotBlank(verifyConditionExpression)) {
            saveable = parseExpressionBoolean(verifyConditionExpression, evaluationContext);
        }
        if (!saveable) {
            return proceedingJoinPoint.proceed();
        }

        String verifyType = verifyAnnotation.type();
        if (StrUtil.isBlank(verifyType)) {
            verifyType = (String) parseExpression(verifyAnnotation.typeExpression(), evaluationContext);
        }
        Long expire = integrityConstraintAnnotation.expire() < 0 ? -1L : integrityConstraintAnnotation.expire() * 1000;
        Map<String, Object> data;
        if (verifyAnnotation.values().length > 0) {
            data = resolve(verifyAnnotation.values(), evaluationContext);
        } else {
            data = resolve(verifyAnnotation.valuesExpression(), evaluationContext);
        }
        Object result = proceedingJoinPoint.proceed();
        IntegrityConstraint integrityConstraint = IntegrityConstraint.builder().businessKey(businessKey)
                .verifyType(verifyType).jsonData(JacksonUtil.objToStr(data))
                .createTime(System.currentTimeMillis())
                .expire(expire)
                .build();
        this.integrityConstraintService.save(integrityConstraint);
        log.info("businessKey={},verifyType={} saves success", integrityConstraint.getBusinessKey(), integrityConstraint.getVerifyType());
        return result;
    }

    private MethodBasedEvaluationContext getEvaluationContext(ProceedingJoinPoint proceedingJoinPoint) {
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        Object[] args = proceedingJoinPoint.getArgs();
        SpelRootObject rootObject = new SpelRootObject(method, args);
        MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, method, args, parameterNameDiscoverer);
        evaluationContext.addPropertyAccessor(new MapAccessor());
        evaluationContext.setBeanResolver(this.beanFactoryResolver);
        return evaluationContext;
    }

    private Object parseExpression(String spelExpression, EvaluationContext evaluationContext) {
        Object object = null;
        try {
            object = spelExpressionParser.parseExpression(spelExpression).getValue(evaluationContext);
        } catch (Exception e) {
            throw new IntegrityConstraintException("获取值失败：" + spelExpression, e);
        }
        if (ObjectUtils.isEmpty(object)) {
            throw new IntegrityConstraintException("获取值失败：" + spelExpression);
        }
        return object;
    }

    private Boolean parseExpressionBoolean(String spelExpression, EvaluationContext evaluationContext) {
        Object object = null;
        try {
            object = spelExpressionParser.parseExpression(spelExpression).getValue(evaluationContext);
        } catch (Exception e) {
            throw new IntegrityConstraintException("非Boolean值：[" + spelExpression + "]=" + object, e);
        }
        if (object == null || !(object instanceof Boolean)) {
            throw new IntegrityConstraintException("非Boolean值：[" + spelExpression + "]=" + object);
        }
        return (Boolean) object;
    }

    private Map<String, Object> resolve(ValueAnnotation[] values, EvaluationContext evaluationContext) {
        Map<String, Object> data = new HashMap<>();
        for (int i = 0; i < values.length; i++) {
            ValueAnnotation valueAnnotation = values[i];
            String valueExpression = valueAnnotation.valueExpression();
            String key = valueAnnotation.key();
            Object evaluated = parseExpression(valueExpression, evaluationContext);
            data.put(key, evaluated);
        }
        return data;
    }

    private Map<String, Object> resolve(String dataExpression, EvaluationContext evaluationContext) {
        Map<String, Object> data = new HashMap<>();
        String[] dataExpressionArray = dataExpression.split(",");
        for (int i = 0; i < dataExpressionArray.length; i++) {
            String[] subDataArray = dataExpressionArray[i].split("->");
            String subDataPath = subDataArray[0];
            Object evaluated = parseExpression(subDataArray[0], evaluationContext);
            String paramName = subDataArray.length == 2 ? subDataArray[1] : subDataPath.substring(subDataPath.lastIndexOf(".") + 1);
            data.put(paramName, evaluated);
        }
        return data;
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}
